﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Evaluant.Uss;
using Evaluant.Uss.Common;
using Evaluant.Uss.Models;
using Evaluant.Uss.Commands;
using Evaluant.OPath;

namespace EussSimpleDBProvider
{
    class SimpleDBEngine : PersistenceEngineImplementation
    {
        Amazon.SimpleDB.AmazonSimpleDBClient m_DBClient = null;
        string m_DomainName = string.Empty;
        SimpleDBCommandProcessor m_Processor = null;

        public SimpleDBEngine(Amazon.SimpleDB.AmazonSimpleDBClient client, string domainName, Model model)
        {
            if (client == null)
                throw new ArgumentNullException("client");

            if(domainName == null)
                throw new ArgumentNullException("domainName");

            _Model = model;
            m_DBClient = client;
            m_DomainName = domainName;

            m_Processor = new SimpleDBCommandProcessor(this);
        }

        List<Amazon.SimpleDB.Model.Item> LoadAllItems(string select, int min, int max)
        {
            List<Amazon.SimpleDB.Model.Item> result = new List<Amazon.SimpleDB.Model.Item>();

            Amazon.SimpleDB.Model.SelectResponse sr = DBClient.Select(new Amazon.SimpleDB.Model.SelectRequest().WithSelectExpression(select));

            result.AddRange(sr.SelectResult.Item);

            while (sr.SelectResult.IsSetNextToken())
            {
                sr = DBClient.Select(new Amazon.SimpleDB.Model.SelectRequest().WithSelectExpression(select).WithNextToken(sr.SelectResult.NextToken));
                result.AddRange(sr.SelectResult.Item);
            }

            return result;
        }

        internal Amazon.SimpleDB.AmazonSimpleDBClient DBClient { get { return m_DBClient; } }
        internal string DomainName { get { return m_DomainName; } }

        internal string IdColumn { get { return DomainName + "_ID_"; } }
        internal string TypeColumn { get { return DomainName + "_Type_"; } }

        internal string GetName(string type, string id)
        {
            return string.Concat(type, "|", id);
        }

        internal string GetTypeFromName(string name)
        {
            int pos = name.IndexOf('|');
            return name.Substring(0, pos);
        }

        internal string GetIdFromName(string name)
        {
            int pos = name.IndexOf('|');
            return name.Substring(pos + 1);
        }

        Dictionary<string, List<string>> AllAttributesForType = new Dictionary<string, List<string>>();
        object AttrDicLock = new object();
        internal string GetSelectListWithComma(string type, params string[] attributes)
        {
            List<string> toselect;
            if (attributes != null && attributes.Length > 0)
            {
                toselect = new List<string>(attributes);
                toselect.Insert(0, TypeColumn);
            }
            else
            {
                lock (AttrDicLock)
                {
                    if (!AllAttributesForType.ContainsKey(type))
                    {
                        foreach (string typename in Model.GetTreeAsArray(type))
                        {
                            List<string> attributelist = new List<string>();
                            attributelist.Add(TypeColumn);

                            foreach (Evaluant.Uss.Models.Attribute attr in Model.GetInheritedAttributes(typename))
                            {
                                if (!attributelist.Contains(attr.Name))
                                {
                                    attributelist.Add(attr.Name);
                                }
                            }

                            AllAttributesForType.Add(type, attributelist);
                        }
                    }
                }

                toselect = AllAttributesForType[type];
            }

            string result = GetCommaString(toselect.ToArray());

            return result;
        }

        public override void InitializeRepository()
        {
            DBClient.DeleteDomain(new Amazon.SimpleDB.Model.DeleteDomainRequest().WithDomainName(DomainName));
            DBClient.CreateDomain(new Amazon.SimpleDB.Model.CreateDomainRequest().WithDomainName(DomainName));
        }

        public override EntitySet LoadWithId(string type, string[] id, string[] attributes)
        {
            EntitySet result = new EntitySet();
            Amazon.SimpleDB.Model.SelectRequest select = new Amazon.SimpleDB.Model.SelectRequest();
            string selectstring = "select " + GetSelectListWithComma(type, attributes) + " from " + DomainName;
            
            string ids = GetCommaAndQuotedString(id);
            string types = GetCommaAndQuotedString(Model.GetTreeAsArray(type));

            selectstring += " where " + IdColumn + " in(" + ids + ") intersection " + TypeColumn + " in(" + types + ")";
            foreach (Amazon.SimpleDB.Model.Item item in LoadAllItems(selectstring, -1, -1))
            {
                result.Add(CreateEntity(item));
            }

            return result;
        }

        internal static string GetCommaAndQuotedString(string[] input)
        {
            string result = string.Empty;

            foreach (string item in input)
            {
                if (result.Length > 0)
                {
                    result += ", ";
                }

                result += "'" + item + "'";
            }

            return result;
        }

        internal static string GetCommaString(string[] input)
        {
            return string.Join(",", input);
        }

        Evaluant.Uss.EntityEntry CreateEntityEntry(Amazon.SimpleDB.Model.Attribute attribute, string entityType)
        {
            Type type = Model.GetAttribute(entityType, attribute.Name, true).Type;
            Evaluant.Uss.EntityEntry entry = new EntityEntry(attribute.Name, SimpleDBConversion.FromSDBString(attribute.Value, type), type, State.UpToDate, null);
            return entry;
        }

        Evaluant.Uss.Entity CreateEntity(Amazon.SimpleDB.Model.Item item)
        {
            string type = GetTypeFromName(item.Name);
            Evaluant.Uss.Entity e = new Evaluant.Uss.Entity(type);

            foreach (Amazon.SimpleDB.Model.Attribute attr in item.Attribute)
            {
                if (attr.Name == IdColumn || attr.Name == TypeColumn)
                    continue;

                Evaluant.Uss.EntityEntry entry = CreateEntityEntry(attr, type);
                e.AddValue(entry.Name, entry.Value, entry.Type, State.UpToDate);
            }

            e.Id = GetIdFromName(item.Name);
            e.State = State.UpToDate;

            return e;
        }

        public override void LoadReference(System.Collections.IEnumerable entities, string[] references)
        {
            //Create list of referencenames for the query
            string referencesstring = GetCommaString(references);

            //Create list of parentids
            string parentids = string.Empty;
            HashSet<string> parentTypes = new HashSet<string>();
            
            Dictionary<string, Evaluant.Uss.Entity> parentLookup = new Dictionary<string, Evaluant.Uss.Entity>();
            foreach (Evaluant.Uss.Entity entity in entities)
            {
                if (parentids.Length > 0)
                {
                    parentids += ", ";
                }

                parentids += "'" + entity.Id + "'";

                if (!parentTypes.Contains(entity.Type))
                {
                    parentTypes.Add(entity.Type);
                }

                parentLookup.Add(entity.Id, entity);

                // mark references as loaded for each entity
                foreach (string refName in references)
                    if (!entity.InferredReferences.Contains(refName))
                        entity.InferredReferences.Add(refName);
            }

            string selectstring = "select " + IdColumn + ", " + referencesstring + " from " + DomainName + " where " + IdColumn + " in (" + parentids + ") intersection " + TypeColumn + " in (" + GetCommaAndQuotedString(parentTypes.ToArray()) + ")";
            Amazon.SimpleDB.Model.SelectResponse selected = DBClient.Select(new Amazon.SimpleDB.Model.SelectRequest().WithSelectExpression(selectstring));

            Dictionary<string, string> child_parent = new Dictionary<string, string>();
            Dictionary<string, List<string>> role_subid = new Dictionary<string, List<string>>();

            foreach (var item in selected.SelectResult.Item)
            {
                string parentid = item.Attribute[0].Value;

                for (int i = 1; i < item.Attribute.Count; i++)
                {
                    string role = item.Attribute[i].Name;
                    string subid = GetIdFromName(item.Attribute[i].Value);
                    string subtype = GetTypeFromName(item.Attribute[i].Value);

                    if (!role_subid.ContainsKey(role))
                        role_subid.Add(role, new List<string>());

                    role_subid[role].Add(subid);;

                    child_parent.Add(subid, parentid);
                }
            }

            foreach (string role in references)
            {
                if (role_subid.ContainsKey(role))
                {
                    string childType = Model.GetReference(parentTypes.First(), role).ChildType;

                    EntitySet set = LoadWithId(childType, role_subid[role].ToArray());

                    foreach (Evaluant.Uss.Entity subent in set)
                    {
                        Evaluant.Uss.Entity entity = parentLookup[child_parent[subent.Id]];

                        State tempState = entity.State;

                        entity.AddValue(role, subent, State.UpToDate);

                        entity.State = tempState;
                    }
                }
            }
        }

        public override void ProcessCommands(Transaction tx)
        {
            foreach (Command c in tx.PendingCommands)
                c.Accept(m_Processor);
        }

        public override object LoadScalar(string opath)
        {
            throw new NotSupportedException();
        }

        public override EntitySet Load(Evaluant.OPath.OPathQuery opath, string[] attributes, string orderby, int first, int max)
        {
            EntitySet set = new EntitySet();

            if (first <= 0)
                throw new ArgumentException("first must be greater than 0");

            if (max < 0)
                throw new ArgumentException("max must be none negative");

            opath.Compile();

            if (opath.HasErrors)
                throw new OPathException(opath);

            SimpleDBQueryTransformer2 transformer2 = new SimpleDBQueryTransformer2(Model,this);
            switch (opath.QueryType)
            {
                case OPathQueryTypeEnum.Path:
                    if (opath.Path == null)
                        throw new ArgumentNullException("opath.Path");

                    if (opath.Path.Identifiers.Count == 0)
                        throw new ArgumentNullException("path", "Must have at least one Identifier");

                    opath.Path.Accept(transformer2);
                    break;

                case OPathQueryTypeEnum.Expression:
                    if (opath.Expression == null)
                        throw new ArgumentNullException("query.Expression");

                    opath.Expression.Accept(transformer2);
                    break;
            }

            string simpledbquery = string.Empty;

            foreach (Amazon.SimpleDB.Model.Item item in LoadAllItems(simpledbquery, first, max))
            {
                set.Add(CreateEntity(item));
            }

            return set;
        }

        public override object LoadScalar(Evaluant.OPath.OPathQuery opath)
        {
            throw new NotSupportedException();
        }
    }
}
